package model;

import exception.ContaInexistenteException;
import global.Global;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.Semaphore;

import parser.ContaParser;
import simulacao.Simulacao;

/**
 * Representa uma agência bancária econtrola o acesso a contas locais, ralizando
 * operações e verificando sua consistência. Cada agência possui um servidor
 * para responder a requisições externas.
 * 
 * @author pedrosa
 */
public class Agencia {
	private final String database = "rsc/database";
	private final String databaseExtension = ".txt";
	private final String logfile = "rsc/log";
	private final String logExtension = ".txt";

	private String nome;
	private int codigo;
	private Map<Integer, String> agenciasIP;
	private Map<String, ContaInfo> contas;
	private ControleFila controle;
	private Servidor servidor;
	private StringBuilder logBuilder;

	/**
	 * Inicia uma agência na máquina local, preparando seu servidor para receber
	 * pedidos externos de conexão. A configuração da agência criada depende dos
	 * argumentos passados pela linha de comando. Para mais informações sobre os
	 * argumentos, consulte a documentação em anexo.
	 * 
	 * A agência fica ativa até que o usuário entre com a tecla 'Q'. No caso de
	 * uma simulação sendo gerada na máquina local, a agência só é desativada
	 * após o final do simulação.
	 * 
	 * @param args
	 *            Parâmetros para a agência e a simulação (opcional).
	 */
	public static void main(String[] args) {

		String nome = args[0];
		int codigo = Integer.parseInt(args[1]);
		int terminais = Integer.parseInt(args[2]);
		Map<Integer, String> ips = new HashMap<Integer, String>();

		for (int i = 3; i < args.length; i += 2) {
			ips.put(Integer.parseInt(args[i]), args[i + 1]);
			Global.NUM_AGENCIAS++;
		}

		try {
			Agencia ag = new Agencia(nome, codigo, terminais, ips);
			Simulacao s = null;

			System.out.println("Ativando agência " + ag.getNome());

			if (codigo == 0) {
				s = new Simulacao(ag);
				s.start();
				System.out.println("Simulação iniciada");
			}

			int c = System.in.read();
			while (c == -1 || Character.toUpperCase(c) == 'Q') {
				Thread.sleep(100);
				c = System.in.read();
			}

			if (s != null)
				s.join();

			ag.servidor.desligar();
			ag.closeAgencia();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		System.out.println("Agência desativada");
	}

	/**
	 * Classe de controle para a classe Conta. Permite saber se uma conta está
	 * bloqueada (em uso), bloquear e desbloquear uma conta, além de enfileirar
	 * os pedidos de operação sobre uma conta quando a mesma já estiver em uso.
	 * 
	 * @author pedrosa
	 */
	private class ContaInfo {
		private Conta conta;
		private Semaphore semAcesso;

		public ContaInfo(Conta conta) {
			this.conta = conta;
			this.semAcesso = new Semaphore(1);
		}

		public Conta getConta() {
			return conta;
		}

		public void getAcesso() throws InterruptedException{
			this.semAcesso.acquire();
		}
		
		public void liberaAcesso(){
			this.semAcesso.release();
		}
		
	}

	/**
	 * 
	 * @param nome
	 *            Nome da agência bancária
	 * @param codigo
	 *            Código numérico da agência
	 * @param numTerminais
	 *            Quantidade de terminais de atendimento
	 * @param agenciasIP
	 *            Relação de códigos de agências e seus respectivos IPs
	 * @throws IOException
	 */
	public Agencia(String nome, int codigo, int numTerminais,
			Map<Integer, String> agenciasIP) throws IOException {
		this.nome = nome;
		this.codigo = codigo;
		this.agenciasIP = agenciasIP;
		this.contas = new HashMap<String, ContaInfo>();
		this.controle = new ControleFila(this, numTerminais);
		this.servidor = new Servidor(this);

		logBuilder = new StringBuilder();

		new Thread(this.servidor).start();

		readDatabase();
	}

	/**
	 * Encerra a operação da agência, salvando seu banco de dados e o registro
	 * de operações (arquivo de log).
	 * 
	 * @throws IOException
	 */
	public void closeAgencia() throws IOException {
		saveDatabase();
		saveLog();
	}

	/**
	 * Carrega as informações o banco de dados para a memória principal.
	 * 
	 * @throws FileNotFoundException
	 */
	private void readDatabase() throws FileNotFoundException {
		contas.clear();

		ContaParser parser = new ContaParser(new FileReader(database + codigo
				+ databaseExtension));
		parser.parse();
		List<Conta> list = parser.output;

		for (Conta c : list) {
			contas.put(c.getCodConta(), new ContaInfo(c));
		}
	}

	/**
	 * Salva as alterações no banco de dados.
	 * 
	 * @throws IOException
	 */
	private void saveDatabase() throws IOException {
		File f = new File(database + codigo + databaseExtension);
		BufferedWriter bw = new BufferedWriter(new FileWriter(f));
		StringBuilder sb = new StringBuilder();

		for (ContaInfo ci : this.contas.values()) {
			sb.append(ci.getConta().getNome());
			sb.append(":");
			sb.append(ci.getConta().getCodConta());
			sb.append(":");
			sb.append(ci.getConta().getValorInteiro().intValue());
			sb.append(",");
			sb.append(ci.getConta().getValorDecimal().intValue());
			sb.append(":");
			sb.append(ci.getConta().getChequeInteiro().intValue());
			sb.append(",");
			sb.append(ci.getConta().getChequeDecimal().intValue());
			sb.append("\n");
		}

		bw.write(sb.toString());
		bw.flush();
		bw.close();
	}

	/**
	 * Salva o registro de ocorrẽncias (arquivo de logs).
	 * 
	 * @throws IOException
	 */
	private void saveLog() throws IOException {
		Calendar calendar = Calendar.getInstance();
		SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy");
		File f = new File(this.logfile + this.getCodigo() + "-"
				+ dateFormat.format(calendar.getTime()) + this.logExtension);
		BufferedWriter bw = new BufferedWriter(new FileWriter(f));

		bw.write(logBuilder.toString());

		bw.flush();
		bw.close();
	}

	public ControleFila getControle() {
		return controle;
	}

	/**
	 * Classe auxiliar para coordenar o acesso aos terminais. Toda vez que um
	 * cliente chega à agência e nenhum terminal está disponível, ele fica em
	 * espera até que o terminal seja liberado.
	 * 
	 * @author pedrosa
	 */
	public class ControleFila {
		public Semaphore semaforoLivres;
		public Queue<Terminal> filaLivres;
		private List<Terminal> terminais;

		/**
		 * 
		 * @param agencia
		 *            A agência onde os terminais estão fisicamente localizados.
		 * @param numTerminais
		 *            Quantidade de terminais a serem criados.
		 */
		public ControleFila(Agencia agencia, int numTerminais) {
			this.terminais = new ArrayList<Terminal>();

			for (int i = 0; i < numTerminais; i++)
				terminais.add(new Terminal(agencia, i + 1));

			filaLivres = new LinkedList<Terminal>();
			filaLivres.addAll(terminais);
			semaforoLivres = new Semaphore(terminais.size());
		}

		/**
		 * Obtém um terminal. Se nenhum terminal estiver disponível, o cliente
		 * que requisitou ficará enfileirada até que um terminal se torne
		 * disponível.
		 * 
		 * @return
		 * @throws InterruptedException
		 */
		public Terminal getTerminal() throws InterruptedException {
			Terminal t;
			synchronized (filaLivres) {
				t = filaLivres.poll();
			}
			return t;
		}

		/**
		 * Libera um terminal para ser utilizado por outros clientes.
		 * 
		 * @param t
		 * @throws InterruptedException
		 */
		public void liberarTerminal(Terminal t) throws InterruptedException {
			synchronized (filaLivres) {
				filaLivres.add(t);
			}
			semaforoLivres.release();
		}
	}

	/**
	 * Obtém o nome da agẽncia bacária.
	 * 
	 * @return
	 */
	public String getNome() {
		return nome;
	}

	/**
	 * Obtém o código numérico da agência bancária.
	 * 
	 * @return
	 */
	public int getCodigo() {
		return codigo;
	}

	/**
	 * Obtém o endereço IP do servidor uma agência, dado o código da mesma.
	 * 
	 * @param codAgencia
	 * @return
	 */
	public String getIP(Integer codAgencia) {
		return this.agenciasIP.get(codAgencia);
	}

	/**
	 * Inicia uma sessão com uma conta, bloqueando outras operações nesta conta
	 * até que a sessão atual seja encerrada. Se a conta já estiver em uso, a
	 * entidade que resquisitou a operação ficará enfileirada até que a conta
	 * seja liberada.
	 * 
	 * @param codConta
	 *            Código da conta a acessar
	 * @param r
	 *            A thread que requereu o bloqueio da conta
	 * @throws ContaInexistenteException
	 * @throws InterruptedException
	 */
	public void acessoConta(String codConta, Runnable r)
			throws ContaInexistenteException, InterruptedException {
		ContaInfo c = this.contas.get(codConta);
		if (c == null) {
			throw new ContaInexistenteException(codConta, this.getCodigo());
		}
		
		c.getAcesso();
	}

	/**
	 * Encerra a sessão de uma conta especificada, liberando-a para acesso por
	 * outros.
	 * 
	 * @param codConta
	 *            O código da conta a liberar
	 * @throws ContaInexistenteException
	 * @throws InterruptedException
	 */
	public void liberaConta(String codConta) throws ContaInexistenteException,
			InterruptedException {
		ContaInfo c = this.contas.get(codConta);
		if (c == null) {
			throw new ContaInexistenteException(codConta, this.getCodigo());
		}

		c.liberaAcesso();		
	}

	/**
	 * Efetua a retirada de uma quantia de uma conta. Retirada não é o mesmo que
	 * saque, pois consiste na redução de um valor que será utilizado para
	 * efetuar outras operações. No estado atual do aplicativo, uma tal outra
	 * operação pode ser um depósito em outra conta, ou seja, constitui a
	 * primeira etapa de uma transferência.
	 * 
	 * @param inteiro
	 * @param decimal
	 * @param codConta
	 * @param r
	 * @return
	 * @throws ContaInexistenteException
	 * @throws InterruptedException
	 */
	public boolean retirada(Integer inteiro, Integer decimal, String codConta,
			Runnable r) throws ContaInexistenteException, InterruptedException {
		acessoConta(codConta, r);
		ContaInfo ci = this.contas.get(codConta);

		if (ci == null) {
			throw new ContaInexistenteException(codConta, this.getCodigo());
		}

		Conta c = ci.getConta();
		boolean sucesso = false;

		Integer[] total = c.getTotal();

		if (total[0] > inteiro || (total[0] == inteiro && total[1] >= decimal)) {
			c.subtrai(inteiro, decimal);

			synchronized (logBuilder) {
				logBuilder
						.append("Operacao: Retirada\n   Conta: "
								+ codConta
								+ "\n   Valor retirado: R$"
								+ inteiro
								+ ","
								+ (decimal == 0 ? "00" : Global
										.correctDecimal(decimal))
								+ "\n   Valor atual: R$"
								+ c.getValorInteiro()
								+ ","
								+ (c.getValorDecimal() == 0 ? "00" : Global
										.correctDecimal(c.getValorDecimal()))
								+ "\n   Valor cheque especial: R$"
								+ c.getChequeInteiro()
								+ ","
								+ (c.getChequeDecimal() == 0 ? "00" : Global
										.correctDecimal(c.getChequeDecimal()))
								+ "\n");
			}

			sucesso = Boolean.TRUE;
		}
		liberaConta(codConta);

		return sucesso;
	}

	/**
	 * Realiza um depósito em uma conta.
	 * 
	 * @param inteiro Valor inteiro do depósito
	 * @param decimal Valor dos centavos do depósito
	 * @param codConta Código da conta que recebe o depósito
	 * @param r Thread que requisita o depósito
	 * @throws ContaInexistenteException
	 * @throws InterruptedException
	 */
	public void deposito(Integer inteiro, Integer decimal, String codConta,
			Runnable r) throws ContaInexistenteException, InterruptedException {
		acessoConta(codConta, r);
		ContaInfo ci = this.contas.get(codConta);

		if (ci == null) {
			throw new ContaInexistenteException(codConta, this.getCodigo());
		}

		Conta c = ci.getConta();

		c.soma(inteiro, decimal);

		synchronized (logBuilder) {
			logBuilder.append("Operacao: Deposito\n   Conta: "
					+ codConta
					+ "\n   Valor depositado: R$"
					+ inteiro
					+ ","
					+ (decimal == 0 ? "00" : Global.correctDecimal(decimal))
					+ "\n   Valor atual: R$"
					+ c.getValorInteiro()
					+ ","
					+ (c.getValorDecimal() == 0 ? "00" : Global
							.correctDecimal(c.getValorDecimal())) + "\n");
		}

		liberaConta(codConta);
	}

}
